#include <syncup.h>
#include <list.h>
#include <types.h>
#include <common.h>
#include <usb_media.h>
#include <fsnotify.h>
#include <syncup_db.h>
#include <utils.h>

#include <time.h>

//#include <pthread.h> // already include by <syncup.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <glib-object.h>



static struct syncup __real_syncup;
struct syncup *syncup = &__real_syncup;

struct syncup_event *
syncup_event_new(enum syncup_event_type type, void *data,
        syncup_evt_hdlr_t hdlr, syncup_evt_hdlr_t flzr)
{
    struct syncup_event *sc_evt = g_slice_new(struct syncup_event);
    sc_evt->type = type;
    sc_evt->data = data;
    sc_evt->handler = hdlr;
    sc_evt->finalizer = flzr;
    return sc_evt;
}

void
syncup_event_free(struct syncup_event *event)
{
    if (event->finalizer && event->data) {
        event->finalizer(event->data);
    }
    g_slice_free(struct syncup_event, event);
}

void
syncup_event_push(struct syncup_event *event)
{
    async_queue_push(&syncup->evt_queue, event);
}


struct syncup_event *
syncup_event_pop()
{
    return (struct syncup_event *)async_queue_pop(&syncup->evt_queue);
}


struct syncup_event_worker {
    pthread_t tid;
    struct async_queue evt_queue;
    bool running;
};

static struct syncup_event_worker sc_evt_worker[1] = {
        {0, ASYNC_QUEUE_INITIALIZER(sc_evt_worker[0].evt_queue), false}
};

static struct syncup_event_worker *event_dispatch_table[SC_EVENT_NUM] = {
        &sc_evt_worker[0],
        &sc_evt_worker[0],
        &sc_evt_worker[0]
};

static inline bool
syncup_event_worker_is_running(struct syncup_event_worker *worker)
{
    return worker->running;
}

static inline void
syncup_event_worker_stop(struct syncup_event_worker *worker)
{
    worker->running = false;
}


static void *
syncup_event_worker_thread(struct syncup_event_worker *worker)
{
    struct syncup_event *sc_evt;
    struct timespec tv = {5, 0}; /* Timeout for waiting new event */

    syncup_db_thread_init();

    while (1) {
        async_queue_lock(&worker->evt_queue);
        sc_evt = (struct syncup_event *) async_queue_timeout_pop_unlocked(
                &worker->evt_queue, &tv);
        if (sc_evt == NULL) {
            syncup_event_worker_stop(worker);
            async_queue_unlock(&worker->evt_queue);
            break;
        }
        async_queue_unlock(&worker->evt_queue);

        sc_evt->handler(sc_evt->data);
        syncup_event_free(sc_evt);
    }

    syncup_db_thread_end();
    return NULL;
}

static inline void
syncup_event_worker_start(struct syncup_event_worker *worker)
{
    pthread_attr_t attr;

    worker->running = true;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (pthread_create(&worker->tid, &attr,
            (pthread_func_t) syncup_event_worker_thread, worker)) {
        g_critical("can't create syncup event worker thread");
    }
}

void
syncup_event_dispatch(struct syncup_event *event)
{
    struct syncup_event_worker *worker;
    bool worker_is_running;

    g_assert(event->type < SC_EVENT_NUM);
    worker = event_dispatch_table[event->type];

    async_queue_lock(&worker->evt_queue);
    async_queue_push_unlocked(&worker->evt_queue, event);
    worker_is_running = syncup_event_worker_is_running(worker);
    async_queue_unlock(&worker->evt_queue);
    if (!worker_is_running) {
        syncup_event_worker_start(worker);
    }
}

static void
init_syncup(struct syncup *sc)
{
    const char *user_home = g_get_home_dir();
    sc->local_home = malloc(strlen(user_home) + SYNCUP_HOME_DIR_LEN + 2);
    sprintf(sc->local_home, "%s/%s", user_home, SYNCUP_HOME_DIR);
    sc->remote_home = NULL;

    async_queue_init(&sc->evt_queue);

    sc->log = NULL;
}

static void
free_syncup(struct syncup *sc)
{
    free(sc->local_home);
    if (sc->remote_home) {
        free(sc->remote_home);
    }
    if (sc->remote_root) {
        free(sc->remote_root);
    }

    async_queue_free(&sc->evt_queue);

    if (sc->log) {
        fclose(sc->log);
        sc->log = NULL;
    }
}

static void
check_syncup_local_home(struct syncup *sc)
{
    if (g_access(sc->local_home, F_OK) < 0) {
        g_mkdir(sc->local_home, S_IRWXU | S_IRGRP | S_IROTH);
    }
}

struct rsync_laucher_param {
    const char *fifo;
    const char *src;
    const char *dest;
};

static void *
rsync_launcher(struct rsync_laucher_param *param)
{
    GString *cmd = g_string_sized_new(1024);
    int ret;
    g_string_printf(cmd, "rsync -vr --files-from=%s %s %s", param->fifo,
            param->src, param->dest);
    g_debug("cmd: %s", cmd->str);
    ret = system(cmd->str);
    g_string_free(cmd, TRUE);
    return GINT_TO_POINTER(ret);
}

static void
sync_local_and_remote(void *noarg)
{
    struct syncup_db_cursor *c;
    union syncup_db_collection data;

    c = store_modified_tuples_result();
    if (c == NULL) {
        return;
    }

    int need_sync = syncup_fetch_row(c, &data);
    if (!need_sync) {
        return;
    }

    GString *fifo = g_string_new(syncup->local_home);
    g_string_append(fifo, "/file-list");

    if (g_access(fifo->str, F_OK) < 0) {
        g_warning("can't find FIFO '%s'", fifo->str);
    }

    struct rsync_laucher_param rsync_param;
    rsync_param.fifo = fifo->str;
    rsync_param.src = "/";
    rsync_param.dest = syncup->remote_root;
    pthread_t tid;
    if (pthread_create(&tid, NULL, (pthread_func_t) rsync_launcher ,
            &rsync_param)) {
        g_critical("can't open rsync thread");
        return;
    }

    FILE *fp = fopen(fifo->str, "w");
    GString *path = g_string_sized_new(4096);


    g_debug("=== modifed file list begin ===");
    do {
        struct dentry *d = fsnotify_indoe_to_dentry(data.modified.parent);
        dentry_get_path(d, path);
        path_append_component(path, data.modified.name);
        g_debug("%s", path->str);
        fprintf(fp, "%s\n", path->str);

        need_sync = syncup_fetch_row(c, &data);
    } while (need_sync);
    g_debug("=== modifed file list end   ===");

    /* use rsync to sync database */
    g_string_printf(path, "%s/data", syncup->local_home);
    fprintf(fp, "%s\n", path->str);

    fclose(fp);

    pthread_join(tid, NULL);

    g_string_free(path, TRUE);
    g_string_free(fifo, TRUE);
}

static void
um_add_handler(const struct usb_media *um)
{
    g_message("UM_ADD: um->um_root: %s, um->syncup_enabled: %d",
            um->um_root, um->syncup_enabled);
    if (um->syncup_enabled) {
        syncup->remote_root = strdup(um->um_root);
        syncup->remote_home = malloc(strlen(um->um_root)
                + strlen(syncup->local_home) + 1);
        sprintf(syncup->remote_home, "%s%s", um->um_root, syncup->local_home);
        //g_debug("syncup->remote_home: %s", syncup->remote_home);
        GString *remote_db_path = g_string_new(syncup->remote_home);
        g_string_append(remote_db_path, "/data/syncuplocaldb");
        g_debug("remote_db_path: %s", remote_db_path->str);
        attach_remote_db(remote_db_path->str);
        syncup_event_push(syncup_event_new(SC_EVENT_SYNC, NULL,
                sync_local_and_remote, NULL));
        g_string_free(remote_db_path, TRUE);
    }
}

static void
add_dir_to_db(struct dentry *d)
{
    struct syncup_db_entry ent;
    g_assert(d != NULL);
    ent.name = d->name;
    ent.parent = d->parent->inode;
    g_assert(d->parent->inode != 0);
    ent.timestamp = 0; // FIXME: use correct timestamp
    insert_new_tuple(&ent, 0, 1);
    dentry_set_inode(d, ent.inode);
    /* FIXME: before this dentry get its real inode, all file created under
     * it must wait to get correct inode, not 0.
     */
}

static void
del_dir_from_db(void *inode)
{
    struct syncup_db_entry ent;
    ent.inode = (int) GPOINTER_TO_UINT(inode);
    delete_tuple(&ent, WHERE_INODE, 0);
}


struct file_change_param {
    uint32_t parent;
    char *name;
    GString *path;
};

static struct file_change_param *
file_change_param_new(uint32_t parent, const char *name)
{
    struct file_change_param *param = g_slice_new(struct file_change_param);
    param->parent = parent;
    param->name = strdup(name);
    param->path = NULL;
    return param;
}

static void
file_change_param_free(struct file_change_param *param)
{
    free(param->name);
    if (param->path) {
        g_string_free(param->path, TRUE);
    }
    g_slice_free(struct file_change_param, param);
}

static void
update_file_checksum(struct file_change_param *param)
{
    struct syncup_db_entry key_and_ent;
    key_and_ent.parent = (int) param->parent;
    key_and_ent.name = param->name;

    get_sha1sum(param->path->str, key_and_ent.checksum);
    update_tuple(&key_and_ent, &key_and_ent,
            UPDATE_CHECKSUM | WHERE_NAME | WHERE_PARENT, 0);
}

static void
add_file_to_db(struct file_change_param *param)
{
    syncup_add_file_to_db(param->name, param->parent, param->path);
}

static void
del_file_from_db(struct file_change_param *param)
{
    struct syncup_db_entry key;
    key.parent = (int) param->parent;
    key.name = param->name;
    delete_tuple(&key, WHERE_PARENT | WHERE_NAME, 0);
}

static void
fsnotify_evt_handler(struct fsnotify_event *evt)
{
    /* Note: fsnotify_event will be reclaimed after this handler return */
    void *data = NULL;
    syncup_evt_hdlr_t hdlr = NULL;
    syncup_evt_hdlr_t flzr = NULL;

    // TODO: think more about race condition here
    if (G_UNLIKELY(evt->mask & FS_ISDIR)) { /* directory change */
        switch (evt->mask & ~FS_ISDIR) {
        case FS_CREATE:
            data = dentry_lookup_child(evt->d.dir, evt->name);
            hdlr = (syncup_evt_hdlr_t) add_dir_to_db;
            break;
        case FS_DELETE:
            data = GUINT_TO_POINTER(evt->garbage->inode);
            hdlr = del_dir_from_db;
            break;
        case FS_MOVE:
            // TODO: dir FS_MOVE
            break;
        case FS_MOVED_FROM:
            // TODO: dir FS_MOVED_FROM
            break;
        case FS_MOVED_TO:
            // TODO: dir FS_MOVED_TO
            break;
        default:
            g_error("fsnotify_evt_handler: unknown fsnotify_event mask: 0x%x",
                    evt->mask);
        }
    } else { /* file change */
        struct file_change_param *param;
        switch (evt->mask) {
        case FS_MODIFY:
            param = file_change_param_new(evt->d.dir->inode, evt->name);
            param->path = dentry_get_path(evt->d.dir, NULL);
            path_append_component(param->path, evt->name);
            g_debug("FS_MODIFY: %s", param->path->str);

            data = param;
            hdlr = (syncup_evt_hdlr_t) update_file_checksum;
            flzr = (syncup_evt_hdlr_t) file_change_param_free;
            break;
        case FS_CREATE:
            param = file_change_param_new(evt->d.dir->inode, evt->name);
            param->path = dentry_get_path(evt->d.dir, NULL);
            path_append_component(param->path, evt->name);
            g_debug("FS_CREATE: %s", param->path->str);

            data = param;
            hdlr = (syncup_evt_hdlr_t) add_file_to_db;
            flzr = (syncup_evt_hdlr_t) file_change_param_free;
            break;
        case FS_DELETE:
            param = file_change_param_new(evt->d.dir->inode, evt->name);
            data = param;
            hdlr = (syncup_evt_hdlr_t) del_file_from_db;
            flzr = (syncup_evt_hdlr_t) file_change_param_free;
            break;
        case FS_MOVE:
            // TODO:
            break;
        case FS_MOVED_TO:
            // TODO:
            break;
        case FS_MOVED_FROM:
            // TODO:
            break;
        default:
            g_error("fsnotify_evt_handler: unknown fsnotify_event mask: 0x%x",
                    evt->mask);
        }
    }
    syncup_event_push(syncup_event_new(SC_EVENT_FSNOTIFY, data, hdlr, flzr));
}

static void
build_dentry_tree_from_db(struct fsnotify_desc *desc,
        struct dentry *parent, GString *path)
{
    union syncup_db_collection data;
    struct syncup_db_cursor *c = store_entry_tuples_result(parent->inode);
    while (syncup_fetch_row(c, &data)) {
        gsize end = path->len;
        struct dentry *d_child = dentry_new(data.entry.name);
        dentry_set_inode(d_child, data.entry.inode);
        dentry_add_child(parent, d_child);

        path_append_component(path, d_child->name);
        fsnotify_watch_dentry(desc, d_child, path);

        build_dentry_tree_from_db(desc, d_child, path);

        g_string_truncate(path, end);
    }
    syncup_free_result(c);
}

static struct dentry *
dentry_builder(struct fsnotify_desc *desc)
{
    struct dentry *root;
    GString *path = g_string_sized_new(4096);
    g_string_append(path, g_get_home_dir());
    root = dentry_new(path->str);
    dentry_set_inode(root, 1); /* root must have inode = 1 */

    build_dentry_tree_from_db(desc, root, path);

    g_string_free(path, TRUE);
    return root;
}

void
syncup_init()
{
    g_type_init();
    if (!g_thread_supported()) {
        g_thread_init(NULL);
    }

    init_syncup(syncup);
    check_syncup_local_home(syncup);
    //daemonize();

    syncup_service_platform_init();

    usb_media_register_event_handler(UM_ADD, um_add_handler);
    usb_media_start_monitor();

    syncup_db_init(syncup->local_home);
//    if (syncup_db_test_thread_safe()) {
//        g_debug("syncup_db is thread safe");
//    } else {
//        g_debug("syncup_db is not thread safe");
//    }

    fsnotify_register_handler(fsnotify_evt_handler);
    fsnotify_init(dentry_builder);

    fsnotify_print_dentry_tree();
}

void
syncup_run()
{
    struct syncup_event *sc_evt;
    while ((sc_evt = syncup_event_pop()) != NULL) {
        syncup_event_dispatch(sc_evt);
    }
}

void
syncup_end()
{
    fsnotify_end();

    syncup_db_end();

    usb_media_stop_monitor();

    syncup_service_platform_end();
    free_syncup(syncup);
}

void
syncup_add_file_to_db(char *name, uint32_t parent_inode, GString *path)
{
    struct syncup_db_entry ent;
    ent.name = name;
    ent.parent = parent_inode;
    ent.timestamp = 0; // FIMXE: use correct timestamp
    get_sha1sum(path->str, ent.checksum);
    insert_new_tuple(&ent, 0, 0);
}

void
syncup_label_path(const char *path)
{
    struct dentry *d = fsnotify_get_root();
    if (!g_str_has_prefix(path, d->name)) {
        return;
    }
    struct fsnotify_desc *desc = fsnotify_get_desc();
    GString *buf = g_string_new(path + strlen(d->name) + 1);
    GString *path_buf = g_string_new(d->name);

    char *name, *next_name, *p;
    name = strtok_r(buf->str, G_DIR_SEPARATOR_S, &p);
    do {
        struct dentry *d_child = dentry_lookup_child(d, name);
        path_append_component(path_buf, name);
        next_name = strtok_r(NULL, G_DIR_SEPARATOR_S, &p);
        g_debug("syncup_label_path(): path_buf: %s", path_buf->str);
        if (d_child == NULL) {
            if (next_name != NULL) {
                d_child = dentry_new(name);
                dentry_add_child(d, d_child);
                fsnotify_watch_dentry(desc, d_child, path_buf);
                struct syncup_db_entry ent;
                ent.name = name;
                ent.parent = (int) d->inode;
                ent.timestamp = 0; // FIXME: use correct timestamp
                insert_new_tuple(&ent, 0, 1);
                dentry_set_inode(d_child, ent.inode);
            } else { /* last component */
                struct stat st;
                if (g_stat(path_buf->str, &st) < 0) {
                    return;
                }

                if (S_ISDIR(st.st_mode)) {
                    d_child = dentry_new(name);
                    dentry_add_child(d, d_child);
                    fsnotify_watch_dentry_recursively(desc, d_child, path_buf);
                    syncup_add_path_to_db_recursively(name, d->inode, path_buf);
                } else if (S_ISREG(st.st_mode)) { // only handle regular file now
                    syncup_add_file_to_db(name, d->inode, path_buf);
                }
            }
        }
        d = d_child;
        name = next_name;
    } while (name != NULL);

    g_string_free(buf, TRUE);
    g_string_free(path_buf, TRUE);
}
